Skip to content

refactor(quic): verify peer identity from libp2p-TLS extension#764

Closed
tcoratger wants to merge 2 commits into
leanEthereum:mainfrom
tcoratger:worktree-refactor+quic-verify-peer-identity
Closed

refactor(quic): verify peer identity from libp2p-TLS extension#764
tcoratger wants to merge 2 commits into
leanEthereum:mainfrom
tcoratger:worktree-refactor+quic-verify-peer-identity

Conversation

@tcoratger
Copy link
Copy Markdown
Collaborator

Summary

The QUIC connection layer previously generated a random peer ID when the multiaddr lacked a p2p component (in `connect()`) and unconditionally on the server side (in `listen()` / `handle_handshake`). The comment at the synthesis sites explicitly flagged this as NOT correct for production. This PR replaces both fallbacks with real verification of the peer's libp2p TLS extension.

What changed

`tls.py` (+196 lines)

Adds the inverse of the existing certificate generator:

  • `PeerVerificationError` — typed exception for libp2p TLS validation failures.
  • `verify_libp2p_certificate(cert) -> PeerId` — finds the extension by OID `1.3.6.1.4.1.53594.1.1`, parses the ASN.1 `SignedKey ::= SEQUENCE { OCTET STRING, OCTET STRING }` envelope, decodes the protobuf `PublicKey { Type, Data }`, rejects unsupported key types, reconstructs the secp256k1 identity key, verifies the signature over `SIGNATURE_PREFIX || SubjectPublicKeyInfo DER`, and derives the canonical PeerId.
  • Private ASN.1 / protobuf parsing helpers that accept the same three DER length forms used by the encoder.

`connection.py` (+61 / −18)

  • `LibP2PQuicProtocol.init` — server-side instances set `self._quic.tls._request_client_certificate = True`. This is the same mechanism py-libp2p uses on top of aioquic; aioquic does not expose mTLS as a public configuration option.
  • `connect()` — pulls the server cert from the completed TLS session, verifies it, and raises `QuicTransportError("Peer identity mismatch")` if the multiaddr's p2p component disagrees with the verified identity (MITM defense).
  • `listen()` / `handle_handshake` — verifies the client cert; tears down the QUIC session if the cert is missing or fails verification, instead of registering a connection with a synthesized peer ID.

Test plan

  • `uv run --group lint ruff check` on networking — clean
  • `uv run --group lint ruff format --check` — clean
  • `uv run --group lint ty check src/.../networking/ tests/.../networking/` — clean
  • `uv run pytest tests/lean_spec/subspecs/networking/` — 814 passed

Test additions:

  • 7 verifier tests in `test_tls.py`: round-trip, distinct keys, missing extension, tampered signature, malformed outer SEQUENCE tag, unknown KeyType, truncated envelope.
  • `test_connection.py`: replaces the two old random-fallback `connect()` tests with four real-cert tests, including peer-ID mismatch and missing cert. Adds a matching pair for `listen()`.

Things worth a reviewer's eye

  1. `_request_client_certificate` is a private attribute on aioquic's `tls.Context`. It is the only mechanism for mTLS in aioquic today and is what py-libp2p uses. If aioquic ever renames or drops it, the server will silently lose mTLS — worth a follow-up to pin aioquic and add a startup assertion.
  2. No live socket test. The existing test suite never exercised a real network handshake; this PR keeps that style. A real end-to-end test (two `QuicConnectionManager` instances handshaking over 127.0.0.1) would be valuable as a follow-up.
  3. Cross-client interop. `test_roundtrip_recovers_peer_id` only verifies our encoder/decoder agree. A future PR should vendor a known-good cert byte string from rust-libp2p as a test vector to catch divergence from the libp2p TLS spec.

🤖 Generated with Claude Code

tcoratger and others added 2 commits May 23, 2026 23:05
The connection layer previously generated a random peer ID when the
multiaddr lacked a p2p component, and unconditionally on the listen
side. The comment at the synthesis sites explicitly flagged this as
"NOT correct for production".

Adds verify_libp2p_certificate(cert) to tls.py: parses the ASN.1
SignedKey envelope from the libp2p extension (OID 1.3.6.1.4.1.53594.1.1),
decodes the protobuf PublicKey, reconstructs the secp256k1 identity key,
verifies the signature binding the identity key to the TLS public key,
and returns the canonical PeerId via the libp2p multihash derivation.

Wires the verifier into both directions:
- connect(): pulls the server cert from the completed TLS session,
  verifies, and rejects with QuicTransportError on identity mismatch
  with the multiaddr's p2p component (MITM defense).
- listen(): server-side protocol instances now request the client
  certificate via aioquic's mTLS hook so the handshake delivers a peer
  cert in both directions. handle_handshake verifies it and tears down
  the QUIC session if the cert is missing or fails verification.

Test coverage:
- 7 verifier tests in test_tls.py: round-trip, distinct keys, missing
  extension, tampered signature, malformed SEQUENCE, unknown KeyType,
  truncated envelope.
- test_connection.py: replaces the two old random-fallback connect
  tests with four real-cert tests including peer-ID mismatch and
  missing-cert; extends the listen tests with the same.

Note: aioquic does not expose mTLS as a public configuration option, so
the server toggles _request_client_certificate on the TLS context. This
is the same mechanism py-libp2p uses on top of aioquic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…uic-verify-peer-identity

# Conflicts:
#	src/lean_spec/subspecs/networking/transport/quic/tls.py
@tcoratger tcoratger closed this May 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant